import {pad,replaceStr as replaceString} from '../format';

//region dateFormat方法
type TimeUnit='year'|'month'|'day'|'hour'|'minute'|'second';
interface DateLocale{
	weeks?:[string,string,string,string,string,string,string];
	weeksShort?:[string,string,string,string,string,string,string];
	months?:[string,string,string,string,string,string,string,string,string,string,string,string];
	monthsShort?:[string,string,string,string,string,string,string,string,string,string,string,string];
	quarters?:[string,string,string,string];
	am?:string;
	pm?:string;
}
type FormatToken=
	//年
	'yy'|'yyyy'|
	//季度, 2位-简写
	'Q'|'QQ'|
	//月, 3位-简写, 4位-全写
	'M'|'MM'|'MMM'|'MMMM'|
	//日
	'd'|'dd'|
	//一年中第几天
	'D'|'DD'|
	//星期, 0-6(0表示星期天)
	'e'|
	//星期, E 1-7(7表示星期天), EE-简写, EEE-全写
	'E'|'EE'|'EEE'|
	//小时, 0-23
	'H'|'HH'|
	//小时, 1-12
	'h'|'hh'|
	//小时, 0-11
	'K'|'KK'|
	//小时, 1-24
	'k'|'kk'|
	//分钟
	'm'|'mm'|
	//秒
	's'|'ss'|
	//毫秒
	'S'|'SS'|'SSS'|
	//上午, 下午
	'a'|
	//时区
	'Z'|'ZZ';
type TimeTokenFormatter={
	// eslint-disable-next-line no-unused-vars
	[token in FormatToken]:(date:Date,dateLocale:Required<DateLocale>,timezoneOffset?:number) => string|number;
}
interface DateFormatOption{
	dateLocale?:DateLocale;
	timezoneOffset?:number;
}
interface DateFormatToken{
	type:FormatToken|'text';
	value?:string;
}
interface DateFormatCache{
	[key:string]:Array<DateFormatToken>;
}
//endregion
//region dateParse方法
type ParseToken=
	'yy'|'yyyy'|
	'M'|'MMM'|'MMMM'|
	'd'|
	'H'|'k'|'h'|'K'|'a'|
	'm'|'s'|'S'|
	'Z'|'ZZ';
interface DateParseOption{
	dateLocale?:DateLocale;
	timezoneOffset?:number;
}
type ParseTokenGroupMap={
	// eslint-disable-next-line no-unused-vars
	[token in ParseToken]:number;
}
interface DateParseCache{
	[key:string]:{
		map:ParseTokenGroupMap;
		regex:RegExp;
	}
}
//endregion
const MILLISECONDS_IN_DAY=86400000;
const MILLISECONDS_IN_HOUR=3600000;
const MILLISECONDS_IN_MINUTE=60000;
const TIMEZONE_OFFSET_IN_MINUTE=new Date().getTimezoneOffset();
const TIMEZONE_OFFSET=TIMEZONE_OFFSET_IN_MINUTE*MILLISECONDS_IN_MINUTE;
const tokenRegex=/'(?:[^']|'')+'|yy(?:yy)?|M{1,4}|d{1,2}|D{1,2}|E{1,3}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|S{1,3}|Z{1,2}|Q{1,2}|k{1,2}|K{1,2}|e|a/g;
const defaultDateLocale:Required<DateLocale>={
	weeks:['星期日','星期一','星期二','星期三','星期四','星期五','星期六'],
	weeksShort:['周日','周一','周二','周三','周四','周五','周六'],
	months:['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月'],
	monthsShort:['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月'],
	quarters:['春','夏','秋','冬'],
	am:'上午',
	pm:'下午',
};
//region startOfDate
/**
 * 获取一个日期指定单位的开始日期
 * @param date		日期
 * @param unit		时间单位
 * @param [self]	是否修改传入的日期
 * @return 日期
 */
function startOfDate(date:Date,unit:TimeUnit,self?:boolean):Date{
	const time=self?date:new Date(date);
	if(unit==='year'){
		time.setMonth(0,1);
		time.setHours(0,0,0,0);
	}else if(unit==='month'){
		time.setDate(1);
		time.setHours(0,0,0,0);
	}else if(unit==='day'){
		time.setHours(0,0,0,0);
	}else if(unit==='hour'){
		time.setMinutes(0,0,0);
	}else if(unit==='minute'){
		time.setSeconds(0,0);
	}else{
		time.setMilliseconds(0);
	}
	return time;
}
//endregion
//region endOfDate
/**
 * 获取一个日期指定单位的结束日期
 * @param date		日期
 * @param unit		时间单位
 * @param [self]	是否修改传入的日期
 * @return 日期
 */
function endOfDate(date:Date,unit:TimeUnit,self?:boolean):Date{
	const time=self?date:new Date(date);
	if(unit==='year'){
		time.setFullYear(date.getFullYear()+1,0,0);
		time.setHours(23,59,59,999);
	}else if(unit==='month'){
		time.setMonth(date.getMonth()+1,0);
		time.setHours(23,59,59,999);
	}else if(unit==='day'){
		time.setHours(23,59,59,999);
	}else if(unit==='hour'){
		time.setMinutes(59,59,999);
	}else if(unit==='minute'){
		time.setSeconds(59,999);
	}else{
		time.setMilliseconds(999);
	}
	return time;
}
//endregion
//region getDateDiff
function _getDiff(date:number,subtract:number,interval:number):number{
	return Math.floor((date-TIMEZONE_OFFSET)/interval)-
		Math.floor((subtract-TIMEZONE_OFFSET)/interval);
}
/**
 * 获取2个日期间指定单位的时间差
 * @param date		日期1
 * @param subtract	日期2
 * @param unit		时间单位
 * @return			多少个单位
 */
function getDateDiff(date:Date,subtract:Date,unit:TimeUnit):number{
	if(unit==='year'){
		return date.getFullYear()-subtract.getFullYear();
	}else if(unit==='month'){
		return (date.getFullYear()-subtract.getFullYear())*12+date.getMonth()-subtract.getMonth();
	}else if(unit==='day'){
		return _getDiff(date.getTime(),subtract.getTime(),MILLISECONDS_IN_DAY);
	}else if(unit==='hour'){
		return _getDiff(date.getTime(),subtract.getTime(),MILLISECONDS_IN_HOUR);
	}else if(unit==='minute'){
		return _getDiff(date.getTime(),subtract.getTime(),MILLISECONDS_IN_MINUTE);
	}else{
		return _getDiff(date.getTime(),subtract.getTime(),1000);
	}
}
//endregion
//region daysInMonth
/**
 * 当月的天数
 * @param date		通过date查询
 * @param [self]	是否修改传入的日期
 * @return		天数
 */
function daysInMonth(date:Date,self?:boolean):number;
/**
 * 当月的天数
 * @param year	查询的年
 * @param month	查询的月(1-12)
 * @return		天数
 */
function daysInMonth(year:number,month:number):number;
function daysInMonth(dateOrYear:Date|number,selfOrMonth?:number|boolean):number{
	if(typeof dateOrYear==='number'){
		return new Date(dateOrYear,selfOrMonth! as number,0).getDate();
	}else if(selfOrMonth){
		dateOrYear.setMonth(dateOrYear.getMonth()+1,0);
		return dateOrYear.getDate();
	}else{
		return new Date(dateOrYear.getFullYear(),dateOrYear.getMonth()+1,0).getDate();
	}
}
//endregion
//region getDayOfYear
/**
 * 当前日期是一年中的第几天
 * @param date		通过date查询
 * @param [self]	是否修改传入的日期
 * @return 第几天
 */
function getDayOfYear(date:Date,self?:boolean):number;
/**
 * 当前日期是一年中的第几天
 * @param year	查询的年
 * @param month	查询的月(1-12)
 * @param day	查询的日
 * @return		第几天
 */
function getDayOfYear(year:number,month:number,day:number):number;
function getDayOfYear(dateOrYear:Date|number,selfOrMonth?:number|boolean,day?:number):number{
	const isNum=typeof dateOrYear==='number';
	if(isNum){
		// @ts-ignore
		dateOrYear=new Date(dateOrYear,selfOrMonth!-1,day!);
	}
	// @ts-ignore
	return _getDiff(dateOrYear.getTime(),startOfDate(dateOrYear,'year',isNum||selfOrMonth).getTime(),MILLISECONDS_IN_DAY)+1;
}
//endregion
//region dateFormat
const DATEFORMAT_CACHE_SIZE=10;
const dateFormatCache:DateFormatCache={};
const dateFormatCacheKeys:Array<string>=new Array<string>(DATEFORMAT_CACHE_SIZE);
let dateFormatCacheIndex=0;
//region 时间的Formatter
const timeFormatter:TimeTokenFormatter={
	yy(date:Date){
		const y=date.getFullYear()%100;
		return pad(y,2);
	},
	yyyy(date:Date){
		return date.getFullYear();
	},
	Q(date:Date){
		return Math.ceil((date.getMonth()+1)/3);
	},
	QQ(date:Date,dateLocale:Required<DateLocale>){
		return dateLocale.quarters[Math.floor(date.getMonth()/3)];
	},
	M(date:Date){
		return date.getMonth()+1;
	},
	MM(date:Date){
		return pad(date.getMonth()+1,2);
	},
	MMM(date:Date,dateLocale:Required<DateLocale>){
		return dateLocale.monthsShort[date.getMonth()];
	},
	MMMM(date:Date,dateLocale:Required<DateLocale>){
		return dateLocale.months[date.getMonth()];
	},
	d(date:Date){
		return date.getDate();
	},
	dd(date:Date){
		return pad(date.getDate(),2);
	},
	D(date:Date){
		return getDayOfYear(date);
	},
	DD(date:Date){
		return pad(getDayOfYear(date),3);
	},
	e(date:Date){
		return date.getDay();
	},
	E(date:Date){
		return date.getDay()||7;
	},
	EE(date:Date,dateLocale:Required<DateLocale>){
		return dateLocale.weeksShort[date.getDay()];
	},
	EEE(date:Date,dateLocale:Required<DateLocale>){
		return dateLocale.weeks[date.getDay()];
	},
	H(date:Date){
		return date.getHours();
	},
	HH(date:Date){
		return pad(date.getHours(),2);
	},
	h(date:Date){
		return date.getHours()%12||12;
	},
	hh(date:Date){
		return pad(date.getHours()%12||12,2);
	},
	K(date:Date){
		return date.getHours()%12;
	},
	KK(date:Date){
		return pad(date.getHours()%12,2);
	},
	k(date:Date){
		return date.getHours()||24;
	},
	kk(date:Date){
		return pad(date.getHours()||24,2);
	},
	m(date:Date){
		return date.getMinutes();
	},
	mm(date:Date){
		return pad(date.getMinutes(),2);
	},
	s(date:Date){
		return date.getSeconds();
	},
	ss(date:Date){
		return pad(date.getSeconds(),2);
	},
	S(date:Date){
		return Math.floor(date.getMilliseconds()/100);
	},
	SS(date:Date){
		return pad(Math.floor(date.getMilliseconds()/10),2);
	},
	SSS(date:Date){
		return pad(date.getMilliseconds(),3);
	},
	a(date:Date,dateLocale:Required<DateLocale>){
		return date.getHours()<12?dateLocale.am:dateLocale.pm;
	},
	Z(date:Date,dateLocale:Required<DateLocale>,timezoneOffset?:number){
		return formatTimezone(':',timezoneOffset);
	},
	ZZ(date:Date,dateLocale:Required<DateLocale>,timezoneOffset?:number){
		return formatTimezone('',timezoneOffset);
	},
};
//endregion
//region 格式化时区
function formatTimezone(separator:string,timezoneOffset?:number){
	const offset=timezoneOffset!=null?timezoneOffset:TIMEZONE_OFFSET_IN_MINUTE;
	const sign=offset>0?'-':'+';
	const absOffset=Math.abs(offset);
	const hours=Math.floor(absOffset/60);
	const minutes=absOffset%60;
	return sign+pad(hours,2)+separator+pad(minutes,2);
}
//endregion
/**
 * 格式化时间
 * @param date			日期
 * @param format		格式化模板, 可以用 单引号转义
 * @param [option]		选项
 * @return 格式化后的字符串
 */
function dateFormat(date:Date,format:string,option?:DateFormatOption):string{
	if(!format){
		return '';
	}
	const {dateLocale,timezoneOffset}=option||{};
	const locale=dateLocale?Object.assign({},defaultDateLocale,dateLocale):defaultDateLocale;
	//region 校正时区
	if(timezoneOffset!=null&&timezoneOffset!==TIMEZONE_OFFSET_IN_MINUTE){
		date=new Date(date.getTime()-(timezoneOffset-TIMEZONE_OFFSET_IN_MINUTE)*MILLISECONDS_IN_MINUTE);
	}
	//endregion
	const key=format;
	let result='';
	if(!dateFormatCache[key]){
		//region 组装结果并缓存tokens
		const dateFormatTokens:Array<DateFormatToken>=[];
		let match;
		let lastIndex=0;
		let str='';
		while((match=tokenRegex.exec(format))!=null){
			if(match.index!==lastIndex){
				str+=format.slice(lastIndex,match.index);
			}
			lastIndex=tokenRegex.lastIndex;
			const text=match[0];
			if(text in timeFormatter){
				if(str){
					result+=str;
					dateFormatTokens.push({
						type:'text',
						value:str,
					});
					str='';
				}
				result+=timeFormatter[text as FormatToken](date,locale,timezoneOffset);
				dateFormatTokens.push({type:text} as DateFormatToken);
			}else if(text[0]==="'"&&text.length>1){
				str+=replaceString(text.slice(1,text.length-1),"''","'");
			}else{
				str+=text;
			}
		}
		if(lastIndex<format.length){
			str+=format.slice(lastIndex);
		}
		if(str){
			result+=str;
			dateFormatTokens.push({
				type:'text',
				value:str,
			});
		}
		if(dateFormatCacheKeys[dateFormatCacheIndex]!=null){
			delete dateFormatCache[dateFormatCacheKeys[dateFormatCacheIndex]];
		}
		dateFormatCacheKeys[dateFormatCacheIndex]=key;
		dateFormatCache[key]=dateFormatTokens;
		dateFormatCacheIndex=(dateFormatCacheIndex+1)%DATEFORMAT_CACHE_SIZE;
		//endregion
	}else{
		for(const token of dateFormatCache[key]){
			if(token.type==='text'){
				result+=token.value;
			}else{
				result+=timeFormatter[token.type](date,locale,timezoneOffset);
			}
		}
	}
	return result;
}
//endregion
//region dateParse
const dateParseCache:DateParseCache={};
const dateParseCacheKeys:Array<string>=new Array<string>(DATEFORMAT_CACHE_SIZE);
let dateParseCacheIndex=0;
/**
 * 解析时间
 * @param dateStr	日期字符串
 * @param format	格式化模板, 可以用 单引号转义
 * @param [option]	选项
 * @return 日期对象
 */
function dateParse(dateStr:string,format:string,option?:DateParseOption):Date|null{
	const {dateLocale,timezoneOffset}=option||{};
	const locale=dateLocale?Object.assign({},defaultDateLocale,dateLocale):defaultDateLocale;
	const key=JSON.stringify({
		format,
		dateLocale,
	});
	const date:any={
		year:null,
		month:null,
		day:null,
		hour:null,
		minute:null,
		second:null,
		millisecond:null,
		timezoneOffset:null,
	};
	if(!dateParseCache[key]){
		//region 生成正则表达式和分组信息并缓存
		const map={} as ParseTokenGroupMap;
		let index=0;
		const quarters='('+locale.quarters.join('|')+')';
		const monthsShort='('+locale.monthsShort.join('|')+')';
		const months='('+locale.months.join('|')+')';
		const weeksShort='('+locale.weeksShort.join('|')+')';
		const weeks='('+locale.weeks.join('|')+')';
		const am=`(${locale.am}|${locale.pm})`;
		// @ts-ignore
		const regexText=format.replace(tokenRegex,(match:string) => {
			++index;
			if(match==='yy'){
				map.yy=index;
				return '(\\d{1,2})';
			}else if(match==='yyyy'){
				map.yyyy=index;
				return '(\\d{1,4})';
			}else if(match==='Q'){
				return '(\\d)';
			}else if(match==='QQ'){
				return quarters;
			}else if(match==='M'){
				map.M=index;
				return '(\\d{1,2})';
			}else if(match==='MM'){
				map.M=index;
				return '(\\d{2})';
			}else if(match==='MMM'){
				map.MMM=index;
				return monthsShort;
			}else if(match==='MMMM'){
				map.MMMM=index;
				return months;
			}else if(match==='d'){
				map.d=index;
				return '(\\d{1,2})';
			}else if(match==='dd'){
				map.d=index;
				return '(\\d{2})';
			}else if(match==='D'||match==='DD'){
				return '(\\d{1,3})';
			}else if(match==='e'||match==='E'){
				return '(\\d)';
			}else if(match==='EE'){
				return weeksShort;
			}else if(match==='EEE'){
				return weeks;
			}else if(match==='H'){
				map.H=index;
				return '(\\d{1,2})';
			}else if(match==='HH'){
				map.H=index;
				return '(\\d{2})';
			}else if(match==='h'){
				map.h=index;
				return '(\\d{1,2})';
			}else if(match==='hh'){
				map.h=index;
				return '(\\d{2})';
			}else if(match==='K'){
				map.K=index;
				return '(\\d{1,2})';
			}else if(match==='KK'){
				map.K=index;
				return '(\\d{2})';
			}else if(match==='k'){
				map.k=index;
				return '(\\d{1,2})';
			}else if(match==='kk'){
				map.k=index;
				return '(\\d{2})';
			}else if(match==='m'){
				map.m=index;
				return '(\\d{1,2})';
			}else if(match==='mm'){
				map.m=index;
				return '(\\d{2})';
			}else if(match==='s'){
				map.s=index;
				return '(\\d{1,2})';
			}else if(match==='ss'){
				map.s=index;
				return '(\\d{2})';
			}else if(match==='S'){
				map.S=index;
				return '(\\d)';
			}else if(match==='SS'){
				map.S=index;
				return '(\\d{2})';
			}else if(match==='SSS'){
				map.S=index;
				return '(\\d{3})';
			}else if(match==='a'){
				map.a=index;
				return am;
			}else if(match==='Z'){
				map.Z=index;
				return '(Z|[+-]\\d{2}:\\d{2})';
			}else if(match==='ZZ'){
				map.ZZ=index;
				return '(Z|[+-]\\d{2}\\d{2})';
			}else{
				--index;
				if(match[0]==="'"){
					match=replaceString(match.slice(1,match.length-1),"''","'");
				}
				return match.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
			}
		});
		if(dateParseCacheKeys[dateParseCacheIndex]!=null){
			delete dateParseCache[dateParseCacheKeys[dateParseCacheIndex]];
		}
		dateParseCacheKeys[dateParseCacheIndex]=key;
		dateParseCache[key]={map,regex:new RegExp(regexText)};
		dateParseCacheIndex=(dateParseCacheIndex+1)%DATEFORMAT_CACHE_SIZE;
		//endregion
	}
	const {map,regex}=dateParseCache[key];
	const match=dateStr.match(regex);
	//region 获取时间信息
	if(match!=null){
		if(map.yyyy){
			date.year=parseInt(match[map.yyyy]);
		}else if(map.yy){
			date.year=2000+parseInt(match[map.yy]);
		}
		if(map.M){
			date.month=parseInt(match[map.M])-1;
		}else if(map.MMM){
			date.month=locale.monthsShort.indexOf(match[map.MMM]);
		}else if(map.MMMM){
			date.month=locale.months.indexOf(match[map.MMMM]);
		}
		if(map.d){
			date.day=parseInt(match[map.d]);
		}
		if(map.H){
			date.hour=parseInt(match[map.H]);
		}else if(map.k){
			date.hour=parseInt(match[map.k])%24;
		}else if(map.h){
			date.hour=parseInt(match[map.h])%12;
			if(map.a&&match[map.a]===locale.pm){
				date.hour+=12;
			}
		}else if(map.K){
			date.hour=parseInt(match[map.K]);
			if(map.a&&match[map.a]===locale.pm){
				date.hour+=12;
			}
		}
		if(map.m){
			date.minute=parseInt(match[map.m]);
		}
		if(map.s){
			date.second=parseInt(match[map.s]);
		}
		if(map.S){
			date.millisecond=parseInt(match[map.S])*10**(3-match[map.S].length);
		}
		if(timezoneOffset==null&&(map.Z||map.ZZ)){
			const tzString=(map.Z?replaceString(match[map.Z],':',''):match[map.ZZ]);
			// @ts-ignore
			date.timezoneOffset=(tzString[0]==='+'?-1:1)*(60*tzString.slice(1,3)+1*tzString.slice(3,5));
		}
	}else{
		return null;
	}
	//endregion
	const res=new Date(
		date.year,
		date.month,
		date.day,
		date.hour,
		date.minute,
		date.second,
		date.millisecond
	);
	//region 校正时区
	const offset=timezoneOffset!=null?timezoneOffset:date.timezoneOffset;
	if(offset!=null&&offset!==TIMEZONE_OFFSET_IN_MINUTE){
		res.setMinutes(res.getMinutes()+offset-TIMEZONE_OFFSET_IN_MINUTE);
	}
	//endregion
	return res;
}
//endregion
export {
	startOfDate,
	endOfDate,
	getDateDiff,
	daysInMonth,
	getDayOfYear,
	dateFormat,
	dateParse,
};
